Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 41511fc014594742e8f5efbcd77b2412f31cd2c2


Parents : 2603a45
Author : Ivan <e46112d44649266d71fe2193e00a4710>
Signature : T66BB85Valid, signed by author
Date : 2026-07-27T06:23:42-05:00

feat: introduce portable data directory support with MESHCHAT_DATA_DIR and related CLI flags

Changes
Diff

diff --git a/docs/agents/overview.md b/docs/agents/overview.md
index d7bd3b41..8539e449 100644
--- a/docs/agents/overview.md
+++ b/docs/agents/overview.md
@@ -276,6 +276,7 @@ Common overrides (CLI flags usually mirror these):
| `MESHCHAT_HEADLESS` / `--headless` | Do not auto-launch a browser |
| `MESHCHAT_STORAGE_DIR` / `--storage-dir` | App storage root |
| `MESHCHAT_RETICULUM_CONFIG_DIR` / `--reticulum-config-dir` | Reticulum config dir |
+| `MESHCHAT_DATA_DIR` / `--data-dir` | Portable root for storage + Reticulum when those two are unset |
| `MESHCHAT_PUBLIC_DIR` / `--public-dir` | Frontend assets dir |
| `MESHCHAT_AUTH` / `--auth` | Enable web auth |
| `MESHCHAT_NO_HTTPS` / `--no-https` | HTTP instead of HTTPS |

diff --git a/docs/en/getting-started.md b/docs/en/getting-started.md
index 4c05c2a9..e9154da6 100644
--- a/docs/en/getting-started.md
+++ b/docs/en/getting-started.md
@@ -79,10 +79,12 @@ Use the search bar to query both sets at once. MeshChatX guide text is currently
## Storage locations
-| Data | Typical path |
+| Data | Typical path (CLI default) |
| --------------------- | -------------------------------------------------- |
-| MeshChatX app data | `~/.reticulum-meshchatx/` on Linux and macOS |
-| Reticulum config | `~/.reticulum/` |
+| MeshChatX app data | `./storage` (or `--storage-dir` / `MESHCHAT_STORAGE_DIR`) |
+| Reticulum config | `~/.reticulum` (or `--reticulum-config-dir` / `MESHCHAT_RETICULUM_CONFIG_DIR`) |
+| Portable bundle | `<data-dir>/storage` and `<data-dir>/.reticulum` when using `--data-dir` / `MESHCHAT_DATA_DIR` |
+| Desktop Electron data | `~/.reticulum-meshchatx` and `~/.reticulum` unless overridden at launch |
| Per-identity database | `<storage>/identities/<identity_hash>/database.db` |
| Docker volume | `meshchatx-config` mounted at `/config` |

diff --git a/docs/en/installation.md b/docs/en/installation.md
index aa55278d..83b7a7f6 100644
--- a/docs/en/installation.md
+++ b/docs/en/installation.md
@@ -139,8 +139,9 @@ Common flags and environment variables:
| `--ssl-key` | `MESHCHAT_SSL_KEY` | auto | TLS private key path |
| `--headless` | `MESHCHAT_HEADLESS` | false | Do not open a browser |
| `--auth` | `MESHCHAT_AUTH` | false | Require HTTP basic auth for the UI |
-| `--storage-dir` | `MESHCHAT_STORAGE_DIR` | `./storage` | Application data directory |
-| `--reticulum-config-dir` | (see `--help`) | `~/.reticulum` | Reticulum configuration |
+| `--storage-dir` | `MESHCHAT_STORAGE_DIR` | `./storage` | Application data directory |
+| `--reticulum-config-dir` | `MESHCHAT_RETICULUM_CONFIG_DIR` | `~/.reticulum` | Reticulum configuration |
+| `--data-dir` | `MESHCHAT_DATA_DIR` | none | Portable root (`storage` + `.reticulum` subdirs when the two paths above are unset) |
| `--identity-file` | `MESHCHAT_IDENTITY_FILE` | none | Load identity from file |
| `--rns-log-level` | `MESHCHAT_RNS_LOG_LEVEL` | none | Reticulum log level |
| `--auto-recover` | `MESHCHAT_AUTO_RECOVER` | false | Attempt SQLite recovery on start |
@@ -149,6 +150,35 @@ Common flags and environment variables:
CLI flags override environment variables when both are set.
+### Portable installs (removable media, Tails, USB sticks)
+
+MeshChatX already supports relocating all persistent state off the home directory. Use either explicit paths or a single data root:
+
+```bash
+export PERSIST="/media/amnesia/Persistent/meshchatx"
+mkdir -p "$PERSIST"
+
+meshchatx --headless \
+ --data-dir="$PERSIST"
+```
+
+That creates and uses `$PERSIST/storage` for MeshChatX (identities, SQLite, plugins) and `$PERSIST/.reticulum` for Reticulum interfaces and transport config. You can set the same layout with environment variables:
+
+```bash
+export MESHCHAT_DATA_DIR="$PERSIST"
+meshchatx --headless
+```
+
+Equivalent explicit form (overrides any `--data-dir` subpaths when you set these yourself):
+
+```bash
+meshchatx --headless \
+ --storage-dir="$PERSIST/storage" \
+ --reticulum-config-dir="$PERSIST/.reticulum"
+```
+
+On Windows portable Electron builds, storage and Reticulum config default next to the `.exe` when `PORTABLE_EXECUTABLE_DIR` is set. On Linux and macOS desktop builds, Electron still defaults to `~/.reticulum-meshchatx` and `~/.reticulum` unless you pass the flags above (or set `MESHCHAT_DATA_DIR` / `MESHCHAT_STORAGE_DIR` / `MESHCHAT_RETICULUM_CONFIG_DIR` in the environment before launch).
+
## Reticulum manual bundle
The Reticulum HTML manual is fetched from the upstream website **master** branch at build time by default (clearnet ZIP). There is no in-app clearnet refresh. After cloning the repository, or before packaging a release, run:

diff --git a/electron/shellPathGuard.js b/electron/shellPathGuard.js
index 1313190a..45008919 100644
--- a/electron/shellPathGuard.js
+++ b/electron/shellPathGuard.js
@@ -93,6 +93,14 @@ function isAllowedShellPath(targetPath, ctx) {
const userArgv = ctx.getUserProvidedArguments(process.argv);
add(parseArgvFlag(userArgv, "--storage-dir"));
add(parseArgvFlag(userArgv, "--reticulum-config-dir"));
+ const dataDir =
+ parseArgvFlag(userArgv, "--data-dir") ||
+ (process.env.MESHCHAT_DATA_DIR && String(process.env.MESHCHAT_DATA_DIR).trim()) ||
+ null;
+ if (dataDir) {
+ add(path.join(dataDir, "storage"));
+ add(path.join(dataDir, ".reticulum"));
+ }
for (const root of roots) {
if (root && isResolvedPathUnderRoot(resolved, root)) {

diff --git a/meshchatx.rsm b/meshchatx.rsm
index 9433f4d4..01ffb550 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ

diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 6b6b0b12..afcb7bed 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -289,6 +289,7 @@ from meshchatx.src.path_utils import (
get_file_path,
is_path_within_dir,
resolve_log_dir,
+ resolve_meshchat_data_roots,
safe_path_under_dir,
)
from meshchatx.src.path_utils import (
@@ -10504,6 +10505,16 @@ def main():
type=str,
help="Restore the database from the given path (zip or db file) and exit.",
)
+ parser.add_argument(
+ "--data-dir",
+ type=str,
+ default=os.environ.get("MESHCHAT_DATA_DIR"),
+ help=(
+ "Portable data root: uses <dir>/storage and <dir>/.reticulum when "
+ "--storage-dir and --reticulum-config-dir are not set. "
+ "Can also be set via MESHCHAT_DATA_DIR."
+ ),
+ )
parser.add_argument(
"--reticulum-config-dir",
type=str,
@@ -10604,6 +10615,12 @@ def main():
if args.no_crash_recovery:
recovery.disable()
+ args.storage_dir, args.reticulum_config_dir = resolve_meshchat_data_roots(
+ data_dir=args.data_dir,
+ storage_dir=args.storage_dir,
+ reticulum_config_dir=args.reticulum_config_dir,
+ )
+
planned_storage_dir = args.storage_dir
if not planned_storage_dir:
# On Android, prefer user-accessible external storage

diff --git a/meshchatx/src/backend/http/routes/interfaces.py b/meshchatx/src/backend/http/routes/interfaces.py
index 8a85921c..8a4831e8 100644
--- a/meshchatx/src/backend/http/routes/interfaces.py
+++ b/meshchatx/src/backend/http/routes/interfaces.py
@@ -1859,9 +1859,6 @@ def register_interfaces_routes(routes, app):
status=500,
)
- # preview importable interfaces
-
- # preview importable interfaces
@routes.post("/api/v1/reticulum/interfaces/import-preview")
async def import_interfaces_preview(request):
try:

diff --git a/meshchatx/src/frontend/public/meshchatx-docs/en/getting-started.md b/meshchatx/src/frontend/public/meshchatx-docs/en/getting-started.md
index 4c05c2a9..e9154da6 100644
--- a/meshchatx/src/frontend/public/meshchatx-docs/en/getting-started.md
+++ b/meshchatx/src/frontend/public/meshchatx-docs/en/getting-started.md
@@ -79,10 +79,12 @@ Use the search bar to query both sets at once. MeshChatX guide text is currently
## Storage locations
-| Data | Typical path |
+| Data | Typical path (CLI default) |
| --------------------- | -------------------------------------------------- |
-| MeshChatX app data | `~/.reticulum-meshchatx/` on Linux and macOS |
-| Reticulum config | `~/.reticulum/` |
+| MeshChatX app data | `./storage` (or `--storage-dir` / `MESHCHAT_STORAGE_DIR`) |
+| Reticulum config | `~/.reticulum` (or `--reticulum-config-dir` / `MESHCHAT_RETICULUM_CONFIG_DIR`) |
+| Portable bundle | `<data-dir>/storage` and `<data-dir>/.reticulum` when using `--data-dir` / `MESHCHAT_DATA_DIR` |
+| Desktop Electron data | `~/.reticulum-meshchatx` and `~/.reticulum` unless overridden at launch |
| Per-identity database | `<storage>/identities/<identity_hash>/database.db` |
| Docker volume | `meshchatx-config` mounted at `/config` |

diff --git a/meshchatx/src/frontend/public/meshchatx-docs/en/installation.md b/meshchatx/src/frontend/public/meshchatx-docs/en/installation.md
index aa55278d..83b7a7f6 100644
--- a/meshchatx/src/frontend/public/meshchatx-docs/en/installation.md
+++ b/meshchatx/src/frontend/public/meshchatx-docs/en/installation.md
@@ -139,8 +139,9 @@ Common flags and environment variables:
| `--ssl-key` | `MESHCHAT_SSL_KEY` | auto | TLS private key path |
| `--headless` | `MESHCHAT_HEADLESS` | false | Do not open a browser |
| `--auth` | `MESHCHAT_AUTH` | false | Require HTTP basic auth for the UI |
-| `--storage-dir` | `MESHCHAT_STORAGE_DIR` | `./storage` | Application data directory |
-| `--reticulum-config-dir` | (see `--help`) | `~/.reticulum` | Reticulum configuration |
+| `--storage-dir` | `MESHCHAT_STORAGE_DIR` | `./storage` | Application data directory |
+| `--reticulum-config-dir` | `MESHCHAT_RETICULUM_CONFIG_DIR` | `~/.reticulum` | Reticulum configuration |
+| `--data-dir` | `MESHCHAT_DATA_DIR` | none | Portable root (`storage` + `.reticulum` subdirs when the two paths above are unset) |
| `--identity-file` | `MESHCHAT_IDENTITY_FILE` | none | Load identity from file |
| `--rns-log-level` | `MESHCHAT_RNS_LOG_LEVEL` | none | Reticulum log level |
| `--auto-recover` | `MESHCHAT_AUTO_RECOVER` | false | Attempt SQLite recovery on start |
@@ -149,6 +150,35 @@ Common flags and environment variables:
CLI flags override environment variables when both are set.
+### Portable installs (removable media, Tails, USB sticks)
+
+MeshChatX already supports relocating all persistent state off the home directory. Use either explicit paths or a single data root:
+
+```bash
+export PERSIST="/media/amnesia/Persistent/meshchatx"
+mkdir -p "$PERSIST"
+
+meshchatx --headless \
+ --data-dir="$PERSIST"
+```
+
+That creates and uses `$PERSIST/storage` for MeshChatX (identities, SQLite, plugins) and `$PERSIST/.reticulum` for Reticulum interfaces and transport config. You can set the same layout with environment variables:
+
+```bash
+export MESHCHAT_DATA_DIR="$PERSIST"
+meshchatx --headless
+```
+
+Equivalent explicit form (overrides any `--data-dir` subpaths when you set these yourself):
+
+```bash
+meshchatx --headless \
+ --storage-dir="$PERSIST/storage" \
+ --reticulum-config-dir="$PERSIST/.reticulum"
+```
+
+On Windows portable Electron builds, storage and Reticulum config default next to the `.exe` when `PORTABLE_EXECUTABLE_DIR` is set. On Linux and macOS desktop builds, Electron still defaults to `~/.reticulum-meshchatx` and `~/.reticulum` unless you pass the flags above (or set `MESHCHAT_DATA_DIR` / `MESHCHAT_STORAGE_DIR` / `MESHCHAT_RETICULUM_CONFIG_DIR` in the environment before launch).
+
## Reticulum manual bundle
The Reticulum HTML manual is fetched from the upstream website **master** branch at build time by default (clearnet ZIP). There is no in-app clearnet refresh. After cloning the repository, or before packaging a release, run:

diff --git a/meshchatx/src/path_utils.py b/meshchatx/src/path_utils.py
index 9a0cfa74..67bd9aff 100644
--- a/meshchatx/src/path_utils.py
+++ b/meshchatx/src/path_utils.py
@@ -93,6 +93,31 @@ def resolve_log_dir():
return None
+def resolve_meshchat_data_roots(
+ *,
+ data_dir: str | None,
+ storage_dir: str | None,
+ reticulum_config_dir: str | None,
+) -> tuple[str | None, str | None]:
+ """Apply MESHCHAT_DATA_DIR / --data-dir when explicit roots are unset.
+
+ Layout under data_dir:
+ storage/ MeshChatX databases and identity tree
+ .reticulum/ Reticulum interfaces and transport config
+ """
+ root = (data_dir or os.environ.get("MESHCHAT_DATA_DIR") or "").strip()
+ if not root:
+ return storage_dir, reticulum_config_dir
+ root = os.path.abspath(os.path.expanduser(root))
+ out_storage = storage_dir
+ out_reticulum = reticulum_config_dir
+ if not out_storage:
+ out_storage = os.path.join(root, "storage")
+ if not out_reticulum:
+ out_reticulum = os.path.join(root, ".reticulum")
+ return out_storage, out_reticulum
+
+
def request_client_ip(
request: web.Request,
trusted_proxy_cidrs: str | None = None,

diff --git a/tests/backend/test_data_dir_roots.py b/tests/backend/test_data_dir_roots.py
new file mode 100644
index 00000000..22e2985e
--- /dev/null
+++ b/tests/backend/test_data_dir_roots.py
@@ -0,0 +1,51 @@
+# SPDX-License-Identifier: 0BSD
+
+import os
+
+from meshchatx.src.path_utils import resolve_meshchat_data_roots
+
+
+def test_data_dir_unset_passes_through_explicit_roots():
+ storage, rns = resolve_meshchat_data_roots(
+ data_dir=None,
+ storage_dir="/app/storage",
+ reticulum_config_dir="/app/.reticulum",
+ )
+ assert storage == "/app/storage"
+ assert rns == "/app/.reticulum"
+
+
+def test_data_dir_fills_missing_roots(tmp_path, monkeypatch):
+ monkeypatch.delenv("MESHCHAT_DATA_DIR", raising=False)
+ root = tmp_path / "persist"
+ storage, rns = resolve_meshchat_data_roots(
+ data_dir=str(root),
+ storage_dir=None,
+ reticulum_config_dir=None,
+ )
+ assert storage == os.path.join(str(root.resolve()), "storage")
+ assert rns == os.path.join(str(root.resolve()), ".reticulum")
+
+
+def test_data_dir_does_not_override_explicit_storage(tmp_path, monkeypatch):
+ monkeypatch.delenv("MESHCHAT_DATA_DIR", raising=False)
+ root = tmp_path / "persist"
+ storage, rns = resolve_meshchat_data_roots(
+ data_dir=str(root),
+ storage_dir="/custom/storage",
+ reticulum_config_dir=None,
+ )
+ assert storage == "/custom/storage"
+ assert rns == os.path.join(str(root.resolve()), ".reticulum")
+
+
+def test_data_dir_from_env(monkeypatch, tmp_path):
+ root = tmp_path / "tails"
+ monkeypatch.setenv("MESHCHAT_DATA_DIR", str(root))
+ storage, rns = resolve_meshchat_data_roots(
+ data_dir=None,
+ storage_dir=None,
+ reticulum_config_dir=None,
+ )
+ assert storage == os.path.join(str(root.resolve()), "storage")
+ assert rns == os.path.join(str(root.resolve()), ".reticulum")


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────